أطلق العنان لقوة التكرار في بايثون. دليل شامل للمطورين العالميين حول تنفيذ أدوات التكرار المخصصة باستخدام طرق __iter__ و __next__ بأمثلة عملية وواقعية.
تبسيط بروتوكول التكرار في بايثون: نظرة متعمقة في __iter__ و __next__
التكرار هو أحد المفاهيم الأساسية في البرمجة. في بايثون، هو الآلية الأنيقة والفعالة التي تدعم كل شيء بدءًا من حلقات for البسيطة وصولًا إلى خطوط معالجة البيانات المعقدة. أنت تستخدمه كل يوم عندما تتنقل عبر قائمة، أو تقرأ سطورًا من ملف، أو تعمل مع نتائج قاعدة البيانات. ولكن هل تساءلت يومًا عما يحدث تحت الغطاء؟ كيف تعرف بايثون كيفية الحصول على العنصر "التالي" من أنواع مختلفة جدًا من الكائنات؟
تكمن الإجابة في نمط تصميم قوي وأنيق يُعرف باسم بروتوكول التكرار. هذا البروتوكول هو اللغة المشتركة التي تتحدث بها جميع كائنات بايثون الشبيهة بالتسلسل. من خلال فهم وتنفيذ هذا البروتوكول، يمكنك إنشاء كائنات مخصصة خاصة بك متوافقة تمامًا مع أدوات التكرار في بايثون، مما يجعل التعليمات البرمجية الخاصة بك أكثر تعبيرًا وكفاءة في استخدام الذاكرة و"بايثونية" جوهريًا.
سيأخذك هذا الدليل الشامل في جولة عميقة في بروتوكول التكرار. سنكشف النقاب عن السحر وراء طرق `__iter__` و `__next__`، ونوضح الفرق الحاسم بين الكائن القابل للتكرار والمكرر، ونرشدك خلال بناء أدوات التكرار المخصصة الخاصة بك من البداية. سواء كنت مطورًا متوسطًا تتطلع إلى تعميق فهمك لدواخل بايثون أو خبيرًا يهدف إلى تصميم واجهات برمجة تطبيقات أكثر تعقيدًا، فإن إتقان بروتوكول التكرار هو خطوة حاسمة في رحلتك.
"لماذا": أهمية وقوة التكرار
قبل أن نتعمق في التنفيذ الفني، من الضروري أن ندرك سبب أهمية بروتوكول التكرار. تتجاوز فوائده مجرد تمكين حلقات `for`.
كفاءة الذاكرة والتقييم الكسول
تخيل أنك بحاجة إلى معالجة ملف سجل ضخم يبلغ حجمه عدة جيجابايت. إذا قرأت الملف بأكمله في قائمة في الذاكرة، فمن المحتمل أن تستنفد موارد نظامك. تحل أدوات التكرار هذه المشكلة بشكل جميل من خلال مفهوم يسمى التقييم الكسول.
لا تقوم أداة التكرار بتحميل جميع البيانات مرة واحدة. بدلاً من ذلك، تقوم بإنشاء أو جلب عنصر واحد في كل مرة، فقط عندما يُطلب ذلك. تحافظ على حالة داخلية لتذكر مكانها في التسلسل. هذا يعني أنه يمكنك معالجة دفق كبير جدًا من البيانات (نظريًا) بكمية صغيرة وثابتة جدًا من الذاكرة. هذا هو نفس المبدأ الذي يسمح لك بقراءة ملف ضخم سطرًا بسطر دون تعطل برنامجك.
تعليمات برمجية نظيفة وقابلة للقراءة وعالمية
يوفر بروتوكول التكرار واجهة عالمية للوصول التسلسلي. نظرًا لأن القوائم والمجموعات والقواميس والسلاسل وكائنات الملفات وأنواع أخرى كثيرة تلتزم جميعها بهذا البروتوكول، يمكنك استخدام نفس بناء الجملة - حلقة `for` - للعمل معها جميعًا. هذا التوحيد هو حجر الزاوية في قابلية قراءة بايثون.
ضع في اعتبارك هذا الرمز:
الرمز:
my_list = [1, 2, 3]
for item in my_list:
print(item)
my_string = "abc"
for char in my_string:
print(char)
with open('my_file.txt', 'r') as f:
for line in f:
print(line)
لا تهتم حلقة `for` بما إذا كانت تتكرر على قائمة من الأعداد الصحيحة، أو سلسلة من الأحرف، أو أسطر من ملف. إنها ببساطة تطلب من الكائن الحصول على أداة التكرار الخاصة به ثم تطلب بشكل متكرر من أداة التكرار العنصر التالي. هذا التجريد قوي بشكل لا يصدق.
تفكيك بروتوكول التكرار
البروتوكول نفسه بسيط بشكل مدهش، ويتم تعريفه بواسطة طريقتين خاصتين فقط، غالبًا ما تسمى طرق "dunder" (تسطير مزدوج):
- `__iter__()`
- `__next__()`
لفهم هذه الأمور بشكل كامل، يجب علينا أولاً فهم التمييز بين مفهومين مرتبطين ولكن مختلفين: قابل للتكرار و مكرر.
قابل للتكرار مقابل مكرر: تمييز حاسم
غالبًا ما يكون هذا نقطة مربكة للقادمين الجدد، لكن الفرق أمر بالغ الأهمية.
ما هو الكائن القابل للتكرار؟
الكائن القابل للتكرار هو أي كائن يمكن تكراره. إنه كائن يمكنك تمريره إلى وظيفة `iter()` المضمنة للحصول على مكرر. من الناحية الفنية، يعتبر الكائن قابلاً للتكرار إذا كان ينفذ طريقة `__iter__`. الغرض الوحيد من طريقة `__iter__` الخاصة به هو إرجاع كائن مكرر.
تتضمن أمثلة الكائنات المضمنة القابلة للتكرار ما يلي:
- القوائم (`[1, 2, 3]`)
- المجموعات (`(1, 2, 3)`)
- السلاسل (`"hello"`)
- القواميس (`{'a': 1, 'b': 2}` - تتكرر على المفاتيح)
- المجموعات (`{1, 2, 3}`)
- كائنات الملفات
يمكنك اعتبار الكائن القابل للتكرار بمثابة حاوية أو مصدر للبيانات. لا يعرف كيف ينتج العناصر بنفسه، لكنه يعرف كيفية إنشاء كائن يمكنه ذلك: المكرر.
ما هو المكرر؟
المكرر هو الكائن الذي يقوم بالفعل بعمل إنتاج القيم أثناء التكرار. إنه يمثل دفقًا من البيانات. يجب أن ينفذ المكرر طريقتين:
- `__iter__()`: يجب أن تُرجع هذه الطريقة كائن المكرر نفسه (`self`). هذا مطلوب حتى يمكن استخدام المكررات أيضًا حيث يُتوقع وجود كائنات قابلة للتكرار، على سبيل المثال، في حلقة `for`.
- `__next__()`: هذه الطريقة هي محرك المكرر. تقوم بإرجاع العنصر التالي في التسلسل. عندما لا تكون هناك عناصر أخرى لإرجاعها، يجب أن تثير استثناء `StopIteration`. هذا الاستثناء ليس خطأ؛ إنه الإشارة القياسية لبناء التكرار بأن التكرار قد اكتمل.
الخصائص الرئيسية للمكرر هي:
- يحافظ على الحالة: يتذكر المكرر موقعه الحالي في التسلسل.
- ينتج القيم واحدًا في كل مرة: عبر طريقة `__next__`.
- يمكن استنفاده: بمجرد استهلاك المكرر بالكامل (أي أنه أثار `StopIteration`)، يكون فارغًا. لا يمكنك إعادة تعيينه أو إعادة استخدامه. للتكرار مرة أخرى، يجب عليك الرجوع إلى الكائن الأصلي القابل للتكرار والحصول على مكرر جديد عن طريق استدعاء `iter()` عليه مرة أخرى.
بناء أول مكرر مخصص لدينا: دليل خطوة بخطوة
النظرية رائعة، ولكن أفضل طريقة لفهم البروتوكول هي بناؤه بنفسك. لننشئ فئة بسيطة تعمل كعداد، وتتكرر من رقم البداية حتى الحد الأقصى.
المثال 1: فئة عداد بسيطة
سننشئ فئة تسمى `CountUpTo`. عندما تقوم بإنشاء مثيل لها، ستحدد رقمًا أقصى، وعندما تتكرر عليها، فإنها ستنتج أرقامًا من 1 حتى ذلك الحد الأقصى.
الرمز:
class CountUpTo:
"""مكرر يعد من 1 حتى رقم أقصى محدد."""
def __init__(self, max_num):
print("تهيئة كائن CountUpTo...")
self.max_num = max_num
self.current = 0 # سيخزن هذا الحالة
def __iter__(self):
print("تم استدعاء __iter__، وإرجاع الذات...")
# هذا الكائن هو أداة التكرار الخاصة به، لذلك نعيد الذات
return self
def __next__(self):
print("تم استدعاء __next__...")
if self.current < self.max_num:
self.current += 1
return self.current
else:
# هذا هو الجزء الحاسم: إشارة إلى أننا انتهينا.
print("رفع StopIteration.")
raise StopIteration
# كيفية استخدامه
print("إنشاء كائن العداد...")
counter = CountUpTo(3)
print("\nبدء حلقة التكرار...")
for number in counter:
print(f"تم استلام حلقة التكرار: {number}")
تحليل الرمز وشرحه
دعونا نحلل ما يحدث عند تشغيل حلقة `for`:
- التهيئة: `counter = CountUpTo(3)` ينشئ مثيلًا لفئتنا. يتم تشغيل طريقة `__init__`، مع تعيين `self.max_num` إلى 3 و `self.current` إلى 0. تتم الآن تهيئة حالة كائننا.
- بدء الحلقة: عندما يتم الوصول إلى السطر `for number in counter:`، تستدعي بايثون داخليًا `iter(counter)`.
- يتم استدعاء `__iter__`: يستدعي استدعاء `iter(counter)` طريقة `counter.__iter__()` الخاصة بنا. كما ترى من التعليمات البرمجية الخاصة بنا، تقوم هذه الطريقة ببساطة بطباعة رسالة وإرجاع `self`. هذا يخبر حلقة `for`، "الكائن الذي تحتاج إلى استدعاء `__next__` عليه هو أنا!"
- تبدأ الحلقة: الآن حلقة `for` جاهزة. في كل تكرار، ستستدعي `next()` على كائن المكرر الذي تلقته (وهو كائن `counter` الخاص بنا).
- أول استدعاء `__next__`: يتم استدعاء طريقة `counter.__next__()`. `self.current` هو 0، وهو أقل من `self.max_num` (3). يزيد الرمز `self.current` إلى 1 ويعيده. تعين حلقة `for` هذه القيمة للمتغير `number`، ويتم تنفيذ نص الحلقة (`print(...)`).
- ثاني استدعاء `__next__`: تستمر الحلقة. يتم استدعاء `__next__` مرة أخرى. `self.current` هو 1. تتم زيادته إلى 2 وإرجاعه.
- ثالث استدعاء `__next__`: يتم استدعاء `__next__` مرة أخرى. `self.current` هو 2. تتم زيادته إلى 3 وإرجاعه.
- الاستدعاء النهائي `__next__`: يتم استدعاء `__next__` مرة أخرى. الآن، `self.current` هو 3. الشرط `self.current < self.max_num` خاطئ. يتم تنفيذ كتلة `else`، ويتم رفع `StopIteration`.
- إنهاء الحلقة: تم تصميم حلقة `for` لالتقاط استثناء `StopIteration`. عندما تفعل ذلك، فإنها تعرف أن التكرار قد انتهى وتنتهي بأمان. يستمر البرنامج في تنفيذ أي رمز بعد الحلقة.
لاحظ تفصيلاً رئيسيًا: إذا حاولت تشغيل حلقة `for` على نفس كائن `counter` مرة أخرى، فلن تنجح. تم استنفاد المكرر. `self.current` هو بالفعل 3، لذلك فإن أي استدعاء لاحق لـ `__next__` سيرفع `StopIteration` على الفور. هذه نتيجة لكون كائننا هو أداة التكرار الخاصة به.
مفاهيم المكرر المتقدمة والتطبيقات الواقعية
تعتبر العدادات البسيطة طريقة رائعة للتعلم، ولكن القوة الحقيقية لبروتوكول المكرر تظهر عند تطبيقها على هياكل بيانات مخصصة وأكثر تعقيدًا.
مشكلة الجمع بين الكائن القابل للتكرار والمكرر
في مثال `CountUpTo` الخاص بنا، كانت الفئة هي الكائن القابل للتكرار والمكرر. هذا بسيط ولكنه يحتوي على عيب كبير: المكرر الناتج قابل للاستنفاد. بمجرد التكرار عليه، يتم ذلك.
الرمز:
counter = CountUpTo(2)
print("التكرار الأول:")
for num in counter: print(num) # يعمل بشكل جيد
print("\nالتكرار الثاني:")
for num in counter: print(num) # لا يطبع شيئًا!
يحدث هذا لأن الحالة (`self.current`) مخزنة على الكائن نفسه. بعد الحلقة الأولى، يكون `self.current` هو 2، وأي استدعاءات أخرى لـ `__next__` سترفع `StopIteration` فقط. يختلف هذا السلوك عن قائمة بايثون القياسية، والتي يمكنك تكرارها عدة مرات.
نمط أكثر قوة: فصل الكائن القابل للتكرار عن المكرر
لإنشاء كائنات قابلة للتكرار قابلة لإعادة الاستخدام مثل مجموعات بايثون المضمنة، فإن أفضل ممارسة هي فصل الدورين. سيكون كائن الحاوية هو الكائن القابل للتكرار، وسيقوم بإنشاء كائن مكرر جديد تمامًا في كل مرة يتم فيها استدعاء طريقة `__iter__` الخاصة به.
دعنا نعيد هيكلة مثالنا إلى فئتين: `Sentence` (الكائن القابل للتكرار) و `SentenceIterator` (المكرر).
الرمز:
class SentenceIterator:
"""المكرر المسؤول عن الحالة وإنتاج القيم."""
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
# يجب أن يكون المكرر أيضًا كائنًا قابلاً للتكرار، وإرجاع نفسه.
return self
class Sentence:
"""فئة حاوية قابلة للتكرار."""
def __init__(self, text):
# تحتوي الحاوية على البيانات.
self.words = text.split()
def __iter__(self):
# في كل مرة يتم فيها استدعاء __iter__، فإنه ينشئ كائن مكرر جديدًا.
return SentenceIterator(self.words)
# كيفية استخدامه
my_sentence = Sentence('This is a test')
print("التكرار الأول:")
for word in my_sentence:
print(word)
print("\nالتكرار الثاني:")
for word in my_sentence:
print(word)
الآن، يعمل تمامًا مثل القائمة! في كل مرة تبدأ فيها حلقة `for`، فإنها تستدعي `my_sentence.__iter__()`، والتي تنشئ مثيل `SentenceIterator` جديدًا تمامًا بحالته الخاصة (`self.index = 0`). يسمح هذا بتكرارات متعددة ومستقلة على نفس كائن `Sentence`. هذا النمط أكثر قوة بكثير وهو كيف يتم تنفيذ مجموعات بايثون الخاصة.
مثال: مكررات لا نهائية
لا تحتاج المكررات إلى أن تكون محدودة. يمكن أن تمثل تسلسلًا لا نهائيًا من البيانات. هذا هو المكان الذي تكون فيه طبيعتها الكسولة وواحدة تلو الأخرى ميزة كبيرة. لننشئ مكررًا لتسلسل لانهائي من أرقام فيبوناتشي.
الرمز:
class FibonacciIterator:
"""ينشئ تسلسلًا لانهائيًا من أرقام فيبوناتشي."""
def __init__(self):
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
result = self.a
self.a, self.b = self.b, self.a + self.b
return result
# كيفية استخدامه - تحذير: حلقة لا نهائية بدون توقف!
fib_gen = FibonacciIterator()
for i, num in enumerate(fib_gen):
print(f"Fibonacci({i}): {num}")
if i >= 10: # يجب علينا توفير شرط توقف
break
لن يثير هذا المكرر `StopIteration` من تلقاء نفسه. تقع مسؤولية توفير شرط (مثل عبارة `break`) لإنهاء الحلقة على عاتق التعليمات البرمجية التي تستدعيها. هذا النمط شائع في دفق البيانات وحلقات الأحداث والمحاكاة العددية.
بروتوكول المكرر في النظام البيئي لبايثون
يتيح لك فهم `__iter__` و `__next__` رؤية تأثيرهما في كل مكان في بايثون. إنه البروتوكول الموحد الذي يجعل العديد من ميزات بايثون تعمل معًا بسلاسة.
كيف تعمل حلقات `for` *حقًا*
لقد ناقشنا هذا ضمنيًا، ولكن دعنا نجعله صريحًا. عندما تصادف بايثون هذا السطر:
`for item in my_iterable:`
فإنه ينفذ الخطوات التالية خلف الكواليس:
- يستدعي `iter(my_iterable)` للحصول على مكرر. هذا بدوره يستدعي `my_iterable.__iter__()`. لنسمي الكائن الذي تم إرجاعه `iterator_obj`.
- يدخل حلقة `while True` لا نهائية.
- داخل الحلقة، يستدعي `next(iterator_obj)`، والذي بدوره يستدعي `iterator_obj.__next__()`.
- إذا أعاد `__next__` قيمة، فسيتم تعيينها للمتغير `item`، ويتم تنفيذ التعليمات البرمجية داخل كتلة حلقة `for`.
- إذا أثار `__next__` استثناء `StopIteration`، فإن حلقة `for` تلتقط هذا الاستثناء وتخرج من حلقة `while` الداخلية الخاصة بها. اكتمل التكرار.
الفهم والتعبيرات المولدة
يتم تشغيل فهم القائمة والمجموعة والقواميس بواسطة بروتوكول المكرر. عندما تكتب:
`squares = [x * x for x in range(10)]`
تقوم بايثون فعليًا بإجراء تكرار على كائن `range(10)`، والحصول على كل قيمة، وتنفيذ التعبير `x * x` لإنشاء القائمة. وينطبق الشيء نفسه على التعبيرات المولدة، وهي استخدام أكثر مباشرة للتكرار الكسول:
`lazy_squares = (x * x for x in range(1000000))`
لا يؤدي هذا إلى إنشاء قائمة بمليون عنصر في الذاكرة. إنه ينشئ مكررًا (وتحديدًا كائنًا مولدًا) سيحسب المربعات واحدًا تلو الآخر، أثناء التكرار عليه.
المولدات: الطريقة الأبسط لإنشاء المكررات
في حين أن إنشاء فئة كاملة باستخدام `__iter__` و `__next__` يمنحك أقصى قدر من التحكم، إلا أنه يمكن أن يكون مطولًا للحالات البسيطة. توفر بايثون بناء جملة أكثر إيجازًا لإنشاء المكررات: المولدات.
المولد هو دالة تستخدم الكلمة الأساسية `yield`. عند استدعاء دالة مولد، فإنها لا تقوم بتشغيل التعليمات البرمجية. بدلاً من ذلك، تقوم بإرجاع كائن مولد، وهو مكرر كامل.
دعنا نعيد كتابة مثال `CountUpTo` الخاص بنا كمولد:
الرمز:
def count_up_to_generator(max_num):
"""دالة مولد تنتج أرقامًا من 1 إلى max_num."""
print("بدأ المولد...")
current = 1
while current <= max_num:
yield current # يتوقف هنا ويرسل قيمة مرة أخرى
current += 1
print("انتهى المولد.")
# كيفية استخدامه
counter_gen = count_up_to_generator(3)
for number in counter_gen:
print(f"تم استلام حلقة التكرار: {number}")
انظر إلى مدى بساطة ذلك! الكلمة الأساسية `yield` هي السحر هنا. عند مواجهة `yield`، يتم تجميد حالة الدالة، ويتم إرسال القيمة إلى المتصل، وتتوقف الدالة. في المرة التالية التي يتم فيها استدعاء `__next__` على كائن المولد، تستأنف الدالة التنفيذ من حيث توقفت، حتى تصل إلى `yield` آخر أو تنتهي الدالة. عند انتهاء الدالة، يتم رفع `StopIteration` تلقائيًا لك.
تحت الغطاء، أنشأت بايثون تلقائيًا كائنًا بطرق `__iter__` و `__next__`. في حين أن المولدات غالبًا ما تكون الخيار الأكثر عملية، إلا أن فهم البروتوكول الأساسي ضروري لتصحيح الأخطاء وتصميم الأنظمة المعقدة وتقدير كيفية عمل آليات بايثون الأساسية.
أفضل الممارسات والمزالق الشائعة
عند تنفيذ بروتوكول المكرر، ضع هذه الإرشادات في الاعتبار لتجنب الأخطاء الشائعة.
أفضل الممارسات
- فصل الكائن القابل للتكرار والمكرر: بالنسبة لأي كائن حاوية يجب أن يدعم عمليات الاجتياز المتعددة، قم دائمًا بتنفيذ المكرر في فئة منفصلة. يجب أن تُرجع طريقة `__iter__` الخاصة بالحاوية مثيلًا جديدًا لفئة المكرر في كل مرة.
- ارفع `StopIteration` دائمًا: يجب أن تثير طريقة `__next__` `StopIteration` بشكل موثوق للإشارة إلى النهاية. سيؤدي نسيان هذا إلى حلقات لا نهائية.
- يجب أن تكون المكررات قابلة للتكرار: يجب أن تُرجع طريقة `__iter__` الخاصة بالمكرر `self` دائمًا. يسمح هذا باستخدام المكرر في أي مكان يُتوقع وجود كائن قابل للتكرار.
- فضل المولدات من أجل البساطة: إذا كانت منطق المكرر الخاص بك واضحًا ويمكن التعبير عنه كدالة واحدة، فإن المولد يكون دائمًا أكثر نظافة ووضوحًا. استخدم فئة مكرر كاملة عندما تحتاج إلى ربط حالة أو طرق أكثر تعقيدًا بكائن المكرر نفسه.
المزالق الشائعة
- مشكلة المكرر القابل للاستنفاد: كما تمت مناقشته، كن على دراية بأنه عندما يكون الكائن هو المكرر الخاص به، يمكن استخدامه مرة واحدة فقط. إذا كنت بحاجة إلى التكرار عدة مرات، فيجب عليك إما إنشاء مثيل جديد أو استخدام نمط الكائن القابل للتكرار/المكرر المنفصل.
- نسيان الحالة: يجب أن تعدل طريقة `__next__` الحالة الداخلية للمكرر (على سبيل المثال، زيادة فهرس أو تقدم مؤشر). إذا لم يتم تحديث الحالة، فستعيد `__next__` نفس القيمة مرارًا وتكرارًا، مما قد يتسبب في حلقة لا نهائية.
- تعديل مجموعة أثناء التكرار: يمكن أن يؤدي التكرار على مجموعة أثناء تعديلها (على سبيل المثال، إزالة عناصر من قائمة داخل حلقة `for` التي تتكرر عليها) إلى سلوك غير متوقع، مثل تخطي العناصر أو إثارة أخطاء غير متوقعة. من الأسلم عمومًا التكرار على نسخة من المجموعة إذا كنت بحاجة إلى تعديل الأصل.
الخلاصة
بروتوكول المكرر، بطريقتيه البسيطتين `__iter__` و `__next__`، هو حجر الزاوية في التكرار في بايثون. إنه دليل على فلسفة تصميم اللغة: تفضيل الواجهات البسيطة والمتسقة التي تتيح سلوكيات قوية ومعقدة. من خلال توفير عقد عالمي للوصول إلى البيانات التسلسلية، يسمح البروتوكول لحلقات `for` والفهم والأدوات الأخرى التي لا حصر لها بالعمل بسلاسة مع أي كائن يختار التحدث بلغته.
من خلال إتقان هذا البروتوكول، تكون قد أطلقت العنان للقدرة على إنشاء كائنات شبيهة بالتسلسل الخاصة بك والتي هي مواطنون من الدرجة الأولى في النظام البيئي لبايثون. يمكنك الآن كتابة فئات أكثر كفاءة في استخدام الذاكرة عن طريق معالجة البيانات بشكل كسول، وأكثر سهولة من خلال التكامل النظيف مع بناء جملة بايثون القياسي، وفي النهاية، أكثر قوة. في المرة القادمة التي تكتب فيها حلقة `for`، توقف لحظة لتقدير الرقصة الأنيقة لـ `__iter__` و `__next__` التي تحدث تحت السطح مباشرة.